package com.company.exp2.frame;

import com.company.exp2.util.C;
import com.company.exp2.Interface_.Transmittable;
import com.company.exp2.context.Context;
import com.company.exp2.event.Event;
import com.company.exp2.factory.EventFactory;
import com.company.exp2.station.Station;
import com.company.exp2.typeEnum.EventType;

/**
 * @author dyy
 *
 * 发送帧的实现类
 * 发送帧应该知道自己的 source站
 * 发送帧应该知道自己的发送时长
 * 由于简化模型，不用考虑 ACK RTS CTS 等控制帧，Frame 是 帧 的唯一实现，不可被继承
 * 由于简化模型，ACK帧的不存在导致发送帧必须包含源站的实例，从而通知发送事件成功与否，相当于实现了ACK帧的功能
 */
public final class Frame implements Transmittable {

    private final Station sourceStation;        // 源站
    private final long duration;                // 持续时间
    private long startTimestamp;                // 开始传输时间点

    public Frame(Station sourceStation, long duration) {
        this.sourceStation = sourceStation;
        this.duration = duration;
    }

    /**
     * 将要开始传输时调用
     * 注意，二元指数回退相关实现由 station 实现，
     * 在这个情况下，如果传输时发现当前环境中存在正在传输的帧，则二者都将在超时时间后重新传输
     * 由于碰撞时各个站点都不知道碰撞的发生，所有站点无需考虑碰撞，这些事件对站来讲是透明的，只是成功时调用source站的drop帧即可
     *
     * 如果信道为空，则声明下一次信道可传输的事件
     */
    @Override
    public void onAttemptToTransmit(long timestamp) {
        Context context = Context.currentContext;
        if (context.isVacant()) {
            context.setCurrentTransmittingFrame(this);
            context.getTimeLine().announceAnEvent(
                    EventFactory.createInstance(
                            EventType.CHANNEL_FORBIDDEN,
                            this.getStartTimestamp(),
                            this.getSourceStation()
                    )
            );
            announceNextChannelTransmittableTime(context);
        } else {
            Frame collidedFrame = context.getCurrentTransmittingFrame();
            if (context.hasNotCollided()) {
                notifyTransmissionFailed(collidedFrame, timestamp);
                context.setHasCollided(true);
            }
            notifyTransmissionFailed(this, timestamp);
            System.out.println("站 " + this.getSourceStation().getNo() + " 与站 " + collidedFrame.getSourceStation().getNo() + "发生了碰撞");

            // *******************************************************************
            // 这一步的目的有二：
            // 1. 覆盖先前的传输帧，以便在调用forbidden方法时，能够检测是否发生碰撞
            //      检测发生碰撞时必要的，如果未发生碰撞，才能调用 frame 的 onTransmitting
            //      如果forbidden事件发现自身相关站点和当前传输中的帧的源站不同
            //      则会认为是发生了碰撞
            // 2. 如果多余两个站点同时传输帧，按照之前的逻辑是无法让第三个帧碰撞的
            //      如果通过替换当前传输帧的方法，可以实现1，2两个目的
            //
            // ***********************************************
            // **如果有更好的方法能实现上述两个目的，请修改下面的操作**
            // ***********************************************
            // context.setCurrentTransmittingFrame(this);
            //
            //  现在采用在Context中添加一个布尔标记来实现上述目的
            //  其实本来不想在Context中加个标记的，破坏了原有抽象类的简洁
            //  但是为了模型的简单化，只能这么做了
            //
            // *******************************************************************
        }
    }

    /**
     *  声明传输结束事件
     */
    @Override
    public void onTransmitting(long timestamp) {
        Context context = Context.currentContext;
        Event transmissionFinishedEvent = EventFactory.createInstance(
                EventType.FRAME_END_TRANSMIT,
                getStartTimestamp() + getDuration(),
                getSourceStation());
        context.getTimeLine().announceAnEvent(transmissionFinishedEvent);
    }

    /**
     * 提醒上下文传输结束
     */
    @Override
    public void onTransmissionFinished(long timestamp) {
        onTransmissionSuccess(timestamp);
    }

    /**
     * 发送成功后通知源站数据发送成功
     */
    @Override
    public void onTransmissionSuccess(long timestamp) {
        getSourceStation().onFrameTransmissionSucceed(timestamp);
    }

    /**
     * 当传输失败时，需要在超时时间后重新让 station 进行 avoid，然后重传帧
     */
    @Override
    public void onTransmissionError(long timestamp) {
        long timeoutEventOccurTimestamp = getStartTimestamp() + C.TRANSMISSION_TIMEOUT;
        Event timeoutEvent = EventFactory.createInstance(
                EventType.FRAME_TIMEOUT,
                timeoutEventOccurTimestamp,
                getSourceStation());
        Context.currentContext.getTimeLine().announceAnEvent(timeoutEvent);
    }

    public Station getSourceStation() {
        return sourceStation;
    }

    public long getDuration() {
        return duration;
    }

    public long getStartTimestamp() {
        return startTimestamp;
    }

    public void setStartTimestamp(long startTimestamp) {
        this.startTimestamp = startTimestamp;
    }

    private void notifyTransmissionFailed(Frame failedFrame, long timestamp) {
        failedFrame.onTransmissionError(timestamp);
    }

    private void announceNextChannelTransmittableTime(Context context) {
        Event channelTransmittableEvent = EventFactory.createInstance(
                EventType.CHANNEL_TRANSMITTABLE,
                startTimestamp + duration + C.DIFS,
                null);
        context.getTimeLine().announceAnEvent(channelTransmittableEvent);
    }

}
